﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using System.Diagnostics;


namespace Dragoon
{

    public interface IHeightMap
    {
        float this[int x, int y] { get; }
        int Width { get; }
        int Length { get; }
        void Initialise();

    }

    public interface ILandformation
    {
        /// <summary>
        /// Build the landformation on the heightMap
        /// </summary>
        /// <param name="heightMap"></param>
        void Initialise(ref float[,] heightMap);
    }


    delegate float Mapper(Point p);

    public class HeightMap : IHeightMap
    {
        protected float[,] _heightMap;
        protected int _width, _length;
        public List<ILandformation> Landformations = new List<ILandformation>();

        protected Vector2 Center()
        {
            return new Vector2((float)(_width - 1) / 2.0f, (float)(_length - 1) / 2.0f);
        }

        public int Length
        {
            get { return _length; }
        }

        public int Width
        {
            get { return _width; }
        }

        public float this[int x, int y]
        {
            get { return _heightMap[x, y]; }
        }

        protected void LoadHeightData(Texture2D heightMap, float magnify)
        {
            float minimumHeight = float.MaxValue;
            float maximumHeight = float.MinValue;

            _width = heightMap.Width;
            _length = heightMap.Height;

            Color[] heightMapColors = new Color[_width * _length];
            heightMap.GetData(heightMapColors);

            _heightMap = new float[_width, _length];
            for (int x = 0; x < _width; x++)
                for (int y = 0; y < _length; y++)
                {
                    _heightMap[x, y] = heightMapColors[x + y * _width].R;
                    if (_heightMap[x, y] < minimumHeight) minimumHeight = _heightMap[x, y];
                    if (_heightMap[x, y] > maximumHeight) maximumHeight = _heightMap[x, y];
                }

            // normalise height data to be between 0 and 30.0f
            for (int x = 0; x < _width; x++)
                for (int y = 0; y < _length; y++)
                    _heightMap[x, y] = (_heightMap[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * magnify;
        }

        void Map(Mapper f)
        {
            Point p = Point.Zero;
            for (int x = 0; x < _width; x++)
                for (int y = 0; y < _length; y++)
                {
                    p.X = x;
                    p.Y = y;
                    _heightMap[x, y] = f(p);
                }
        }

        


        protected List<Point> GetNeighboursOf(Point p)
        {
            List<Point> result = new List<Point>();
            if (p.X > 0)
            {
                result.Add(new Point(p.X - 1, p.Y));
                if (p.Y > 0)
                    result.Add(new Point(p.X - 1, p.Y - 1));
                if (p.Y < _length - 1)
                    result.Add(new Point(p.X - 1, p.Y + 1));
            }
            if (p.X < _width - 1)
            {
                result.Add(new Point(p.X + 1, p.Y));
                if (p.Y > 0)
                    result.Add(new Point(p.X + 1, p.Y - 1));
                if (p.Y < _length - 1)
                    result.Add(new Point(p.X + 1, p.Y + 1));
            }
            if (p.Y > 0)
                result.Add(new Point(p.X, p.Y - 1));
            if (p.Y < _length - 1)
                result.Add(new Point(p.X, p.Y + 1));
            return result;
        }

        protected Vector3 GetPosition(int x, int y)
        {
            return new Vector3(x, _heightMap[x, y], -y);
        }

        protected Vector3 GetNormal(Point p)
        {
            if (p.X == _width - 1 || p.Y == _length - 1)
                return new Vector3(0, 1, 0);
            Vector3 side1 = GetPosition(p.X + 1, p.Y);
            Vector3 side2 = GetPosition(p.X, p.Y + 1);
            Vector3 r = Vector3.Cross(side1, side2);
            r.Normalize();
            return r;
        }


        public virtual void Initialise()
        {
            Debug.Assert(Landformations != null, "This method can be called only once");
            foreach (var form in Landformations)
                form.Initialise(ref _heightMap);
            Landformations = null;  // release memory for garbage collection
        }

        private float HeightAt(Point p)
        {
            return _heightMap[p.X, p.Y];
        }

        private float GetHeight(Vector2 v)
        {
            var n = v.ToNeighbourArray();
            Vector3 a = n[0].ToVector3Height(HeightAt(n[0]));
            Vector3 b = n[1].ToVector3Height(HeightAt(n[1]));
            Vector3 c = n[2].ToVector3Height(HeightAt(n[2]));
            Vector3 d = n[3].ToVector3Height(HeightAt(n[3]));
            return 0;
        }

        /// <summary>
        /// Returns the line segments created by breaking a straight line into pieces 
        /// that fit right on the heightmap 
        /// </summary>
        /// <param name="start">the 2D start point</param>
        /// <param name="end">the 2D end point</param>
        /// <returns></returns>
        public List<Vector3> GetLineSegments(Vector2 start, Vector2 end)
        {
            throw new NotImplementedException();
        }

    }

    public enum IslandShape { Circle, Square };

    /// <summary>
    /// Determines how the beach is formed
    /// </summary>
    public enum IslandTrimMethod
    {
        /// <summary>
        /// Ground is filled in to construct a beach according to specification
        /// </summary>
        Fill,
        /// <summary>
        /// Towards the land, original land form persists
        /// </summary>
        Merge,
        /// <summary>
        /// Dig a path at touching waterlevel
        /// </summary>
        /// <remarks>
        /// To see water here, make the water level slightly lower than it actually is
        /// </remarks>
        Ditch
    };
    /// <summary>
    /// Modifies the edges of the terrain to form a beach around the terrain
    /// </summary>
    public class HeightMapIslandTrim : HeightMap
    {
        private IHeightMap _source;
        readonly float _shoreLength;
        readonly float _waterLevel;
        readonly float _shoreHeight;
        readonly IslandShape _type;
        readonly IslandTrimMethod _method;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="source"></param>
        /// <param name="shoreLength">the distance from the start of the beach to the water</param>
        /// <param name="waterLevel"></param>
        /// <param name="shoreHeight">the height as measured from the water level</param>
        /// <param name="type"></param>
        /// <param name="method"></param>
        public HeightMapIslandTrim(IHeightMap source,
            float shoreLength,
            float waterLevel,
            float shoreHeight,
            IslandShape type,
            IslandTrimMethod method)
        {
            _source = source;
            _waterLevel = waterLevel;
            _shoreHeight = shoreHeight;
            _shoreLength = shoreLength;
            _type = type;
            _method = method;
        }

        private float DistanceFromEdge(int x, int y)
        {
            float xd = (x < _width / 2) ? x : (_width - 1 - x);
            float yd = (y < _length / 2) ? y : (_length - 1 - y);
            return Math.Min(xd, yd);
        }

        private void SetHeight(int x, int y)
        {
            Vector2 pos = new Vector2(x, y);
            float shore;
            if (_type == IslandShape.Square)
                shore = _shoreLength - DistanceFromEdge(x, y);
            else
            {
                float distanceFromCenter = Vector2.Distance(Center(), pos);
                float landRadius = ((float)_width / 2.0f) - _shoreLength;
                shore = distanceFromCenter - landRadius;
            }
            var oldH = _source[x, y];
            var newH = MathHelper.Lerp(_waterLevel + _shoreHeight,
                                               _waterLevel - _shoreHeight,
                                               shore / (_shoreLength * 2.0f));
            if (shore < 0.0f)
                _heightMap[x, y] = oldH;
            else
            {
                switch (_method)
                {
                    case IslandTrimMethod.Fill:
                        _heightMap[x, y] = newH;
                        break;
                    case IslandTrimMethod.Merge:
                        _heightMap[x, y] = MathHelper.Lerp(oldH, newH, (shore / _shoreLength));
                        break;
                    case IslandTrimMethod.Ditch:
                        var sd = shore / 2.0f - _shoreLength / 2.0f;
                        if (sd < 0.0f) // land side
                            _heightMap[x, y] = MathHelper.Lerp(oldH, newH, (shore / _shoreLength));
                        else  // water side
                            _heightMap[x, y] = MathHelper.Lerp(newH, oldH, (sd / _shoreLength / 2.0f));
                        break;
                }
            }

        }

        public override void Initialise()
        {
            _source.Initialise();
            _width = _source.Width;
            _length = _source.Length;
            _heightMap = new float[_width, _length];
            for (int x = 0; x < _width; x++)
                for (int y = 0; y < _length; y++)
                    SetHeight(x, y);
            // release to allow collection via GC
            _source = null;
            base.Initialise();
        }
    }


    /// <summary>
    /// Creates a 2x2 mirror of a given heightmap.  
    /// Mirrors toward the south and toward the east
    /// </summary>
    public class HeightMapMirror : HeightMap
    {
        private IHeightMap _source;

        /// <summary>
        /// The source is used once and then discarded
        /// </summary>
        /// <param name="source"></param>
        public HeightMapMirror(IHeightMap source)
        {
            _source = source;
        }

        public override void Initialise()
        {
            _source.Initialise();
            _width = _source.Width * 2;
            _length = _source.Length * 2;
            _heightMap = new float[_width, _length];
            for (int x = 0; x < _width; x++)
                for (int y = 0; y < _length; y++)
                {
                    int sx = x < _source.Width ? x : (_width - 1 - x);
                    int sy = y < _source.Length ? y : (_length - 1 - y);
                    _heightMap[x, y] = _source[sx, sy];
                }
            // release reference to source so that it can be collect by GC
            _source = null;
            base.Initialise();
        }

    }


    public class HeightMapPerlin : HeightMap
    {
        readonly float _minimumHeight = 0.0f;
        readonly float _maximumHeight = 30.0f;
        readonly int _seed;
        readonly float _persistence;
        readonly int _octaves;


        private Game _game;

        public HeightMapPerlin(Game g, int width, int length,
            float minHeight, float maxHeight, int seed, float persistence, int octaves)
        {
            _width = width;
            _length = length;
            _game = g;
            _seed = seed;
            _persistence = persistence;
            _octaves = octaves;
            _minimumHeight = minHeight;
            _maximumHeight = maxHeight;

        }

        /// <summary>
        /// Normalize the heightmap.
        /// </summary>
        void Normalise()
        {
            float min = float.MaxValue;
            float max = float.MinValue;

            // Get the lowest and the highest values.
            for (int x = 0; x < _width; ++x)
            {
                for (int y = 0; y < _length; ++y)
                {
                    if (_heightMap[x, y] > max)
                    {
                        max = _heightMap[x, y];
                    }

                    if (_heightMap[x, y] < min)
                    {
                        min = _heightMap[x, y];
                    }
                }
            }

            // If the heightmap is flat, we set it to the average between _minimumHeight and _maximumHeight.
            if (max <= min)
            {
                for (int x = 0; x < _width; ++x)
                {
                    for (int y = 0; y < _length; ++y)
                    {
                        _heightMap[x, y] = (_maximumHeight - _minimumHeight) * 0.5f;
                    }
                }

                return;
            }

            // Normalize the value between 0.0 and 1.0 then scale it between _minimumHeight and _maximumHeight.
            float diff = max - min;
            float scale = _maximumHeight - _minimumHeight;

            for (int x = 0; x < _width; ++x)
            {
                for (int y = 0; y < _length; ++y)
                {
                    _heightMap[x, y] = (_heightMap[x, y] - min) / diff * scale + _minimumHeight;
                }
            }
        }


        private float Noise(int i)
        {
            i = (i << 13) ^ i;
            return (1.0f - ((i * (i * i * 15731 + 789221) + 1376312589) & 0x7FFFFFFF) / 1073741824.0f);
        }

        public void CreateNoise()
        {
            int txi, tzi;

            float freq, amp;

            float xf, tx, fracx;
            float zf, tz, fracz;

            float v1, v2, v3, v4;
            float i1, i2, total;


            // For each height..
            for (int z = 0; z < _length; ++z)
            {
                for (int x = 0; x < _width; ++x)
                {
                    // Scale x and y to the range of 0.0f, 1.0f.
                    xf = (float)x / (float)_width;
                    zf = (float)z / (float)_length;

                    total = 0.0f;

                    // For each octaves..
                    for (int i = 0; i < _octaves; ++i)
                    {
                        // Calculate frequency and amplitude (different for each octave).
                        freq = (float)Math.Pow(2.0, i);
                        amp = (float)Math.Pow(_persistence, i);

                        // Calculate the x, z noise coodinates.
                        tx = xf * freq;
                        tz = zf * freq;

                        txi = (int)tx;
                        tzi = (int)tz;

                        // Calculate the fractions of x and z.
                        fracx = tx - txi;
                        fracz = tz - tzi;

                        // Get noise per octave for these four points.
                        v1 = Noise(txi + tzi * 57 + _seed);
                        v2 = Noise(txi + 1 + tzi * 57 + _seed);
                        v3 = Noise(txi + (tzi + 1) * 57 + _seed);
                        v4 = Noise(txi + 1 + (tzi + 1) * 57 + _seed);

                        // Smooth noise in the x axis.
                        i1 = MathHelper.SmoothStep(v1, v2, fracx);
                        i2 = MathHelper.SmoothStep(v3, v4, fracx);

                        // Smooth in the z axis.
                        total += MathHelper.SmoothStep(i1, i2, fracz) * amp;
                    }

                    // Save to heightmap.
                    _heightMap[x, z] = total;
                }
            }


        }

        public override void Initialise()
        {
            _heightMap = new float[_width, _length];
            CreateNoise();
            Normalise();
            base.Initialise();
        }

    }

    public class HeightMapLoaded : HeightMap, IHeightMap
    {

        private readonly Game Game;

        public HeightMapLoaded(Game g) { Game = g; }

        public override void Initialise()
        {
            LoadHeightData(Game.Content.Load<Texture2D>("heightmap"), 30.0f);
            base.Initialise();
        }
    }


}